1. Prerequisites

None.

2. Introduction

Many questions researchers try to answer in spatial transcriptomics and image analysis are space related. Coordinates of the observations are often scaled to the resolution of the image that is currently in use. In case of the Visium platform, if one exchanges the tissue_lowres_image.png with the tissue_hires_image.png from the 10X Visium output, the coordinates of the barcoded spots are scaled accordingly to ensure that both, coordinates and image, stay aligned. Thus, coordinates can be used to answer spatial questions in relative but not in absolute measures. For instance, the statement: “The necrotic region spans an area of approximately 400pixels.” is useless if pixels are used as a unit the area will always depend on the image resolution. This vignate explains how SPATA2 computes the transition from pixel to SI units for the Visium platform with the pixel scale factor.

library(SPATA2)
library(SPATAData)
library(tidyverse)

# download object
object_t313 <- downloadFromPublication(pub = "kueckelhaus_et_al_2024", id = "UKF313T")

text_size <- theme(text = element_text(size = 17.5))

plotImage(object_t313, img_name = "lowres") + 
  text_size + 
  labs(subtitle = "Low resolution")

# hires image has been registered using `registerImage()`
plotImage(object_t313, img_name = "hires") + 
  text_size + 
  labs(subtitle = "high resolution")
Fig.1 Different resolutions result in different x- and y-scale.Fig.1 Different resolutions result in different x- and y-scale.

Fig.1 Different resolutions result in different x- and y-scale.

3. The pixel scale factor

To answer questions in absolute measures, distances must be converted to units of the *Système international d’unités (SI). That is, micrometers, millimeters, centimeters, etc. This is possible as most spatial -omic studies come with a ground truth.

3.1 Visium

In case of 10X Visium this ground truth is the center to center distance between two adjacent barcoded spots which is always 100um. Using this information, the actual distances and areas can be computed with a pixel scale factor. This factor is computed automatically by initiateSpataObjectVisium().

(If you have multiple images registered in your SPATA2 object, note that, by default, the pixel scale factor from the active image is used.)

# how many um is one pixel in side lengths
getPixelScaleFactor(object_t313, unit = "um")
## [1] 14.56238
## attr(,"unit")
## [1] "um/px"
# how many pixel is one um in side lengths
getPixelScaleFactor(object_t313, unit = "um", switch = TRUE)
## [1] 0.06867008
## attr(,"unit")
## [1] "px/um"
# by default the pixel scale factor of the active image is used
getImageNames(object_t313)
## [1] "lowres" "hires"
# which is image 'lowres' in this example
activeImage(object_t313)
## [1] "lowres"
# how many um is one pixel in side lengths in case of a higher resolution?
getPixelScaleFactor(object_t313, unit = "um", img_name = "hires")
## [1] 4.368715
## attr(,"unit")
## [1] "um/px"

3.2 MERFISH and Co

The term pixel scale factor derives from a time where SPATA2 was solely thought to work with Visium datasets. It has been expanded to include other platforms such as MERFISH which provides coordinates of cells in micrometer units. If the platform provides the coordinates of its observations in SI units the pixel scale factor theoretically isn’t required but for the code of SPATA2 to work, such a scale factor must exist in the SPATA2object. Therefore, it is set automatically by the respective initiateSpataObject*() function. E.g. in MERFISH data, the x- and y-coordinates are provided in um units. Hence, the pixel scale factor of SPATA2 objects initiated with initiateSpataObjectMERFISH() is always 1mm/px.

4. Working with distances

Multiple functions take arguments that, in some way, refer to distance measures. SPATA2 makes use of the units package to work with distances. If not explained otherwise in the documentation you can provide the distance in pixel or in SI units. Behind the scenes input is converted to pixel and aligned with the current resolution. 4.1 Converting distances gives some examples of how this is done.

4.1 Handling distance input and output

Every SI unit of length is a valid distance unit.


validUnitsOfLength()
##  nanometer micrometer millimeter centimeter  decimeter      meter      pixel 
##       "nm"       "um"       "mm"       "cm"       "dm"        "m"       "px"

validUnitsOfLengthSI()
##  nanometer micrometer millimeter centimeter  decimeter      meter 
##       "nm"       "um"       "mm"       "cm"       "dm"        "m"

# wrappers around units::set_units()
as_micrometer(input = "4mm")
## 4000 [um]

as_millimeter(input = "4cm")
## 40 [mm]

Pixel is not a valid distance unit in units. Pixel, however, are important in image analysis. SPATA2 provides the wrappers needed to reconcile both. They are named according to the unit of interest. To transform between pixel and SI units of length the SPATA2 object must be provided as the scale factor is needed. See the documentation of ?is_dist to obtain more information on how distance values must be specified.


### convert from si -> pixel

# example 1: a simple string works
as_pixel(input = "4mm", object = object_t313)
## [1] 274.6803
## attr(,"unit")
## [1] "px"

# example 2: a unit object works, too
unit_input <- units::set_units(x = 4, value = "mm")

unit_input
## 4 [mm]

class(unit_input)
## [1] "units"

as_pixel(input = unit_input, object = object_t313)
## [1] 274.6803
## attr(,"unit")
## [1] "px"

### convert from pixel -> si

# example 1: simple numeric input is interpreted as pixel
as_millimeter(
  input = c(100, 200, 300), 
  object = object_t313
  )
## Units: [mm]
## [1] 1.456238 2.912476 4.368715
# example 2: strings with px suffix work, too
as_micrometer(
  input = c("50px", "200px", "800px"), 
  object = object_t313
)
## Units: [um]
## [1]   728.1191  2912.4764 11649.9056

A major advantage of using SI units of length is that the input and output of functions that depend on space remains the same regardless of the image resolution that changes with every call to exchangeImage().

4.2 Examples

The following are a few examples of where actual distances might come into play.

# specifying x- and y-range while handling images
xrange = c("2.5mm", "6.5mm")
yrange = c("0.5mm", "4.5mm")

# where to set the breaks is a measure of distance, too
breaks <- str_c(0:8, "mm")

# vector of valid distance inputs
print(breaks)
## [1] "0mm" "1mm" "2mm" "3mm" "4mm" "5mm" "6mm" "7mm" "8mm"
axes_add_on <- ggpLayerAxesSI(object_t313, unit = "mm", breaks = breaks)

rect_add_on <- 
  ggpLayerRect(
    object = object_t313, 
    xrange = xrange, 
    yrange = yrange
    )

plotImage(object = object_t313) + 
  rect_add_on +
  axes_add_on + 
  text_size

plotImage(
  object = object_t313,
  xrange = xrange, # crop the image with distance input
  yrange = yrange  # crop the image with distance input
  ) + 
  text_size
Fig.4 Using SI units to specify distances and ranges.Fig.4 Using SI units to specify distances and ranges.

Fig.4 Using SI units to specify distances and ranges.

plotImage(object_t313) + 
  ggpLayerAxesSI(object_t313, unit = "mm", breaks = breaks) + 
  ggpLayerSpatAnnOutline(object_t313, ids = "necrotic_center") + 
  ggpLayerHorizonSAS(object_t313, id = "necrotic_center", distance = "2mm") + 
  ggpLayerScaleBarSI(object_t313, sb_dist = "2mm", sb_pos = c("5.5mm", "7.5mm"), text_size = 15, text_nudge_y = 10) + 
  text_size
Fig.5 Using SI units for multiple aspects.

Fig.5 Using SI units for multiple aspects.

5. Working with areas

In addition to distances, areas can be computed, too. This becomes interesting, for instance, if the area of an image annotation is of relevance to the biological question.

5.1 Handling area input and output

validUnitsOfArea()
## [1] "nm2" "um2" "mm2" "cm2" "dm2" "m2"  "px"

Pixel are by definition squares which means that all sides are of the same length. This property is used in SPATA2 to use pixel as a unit of distance. It can, however, be a unit of area, too. E.g. 4px with a side length of 1um cover an area of 4um2 or an area of 4px. However, the number of pixels that cover a specific area again depends on the resolution of the image. Again, SPATA2 implements SI units of area.

validUnitsOfAreaSI()
## [1] "nm2" "um2" "mm2" "cm2" "dm2" "m2"

Pixel is not among them. SPATA2 allows resolution dependent conversion from pixel to SI units and vice versa. As pixel are squares of equal sizes the scale factor used for distance conversion can be used in its squared form.

getPixelScaleFactor(object_t313, unit = "um2")
## [1] 212.063
## attr(,"unit")
## [1] "um2/px"
# fewer um2 per pixel in the high resolution object 
# -> pixels are smaller
getPixelScaleFactor(object_t313, unit = "um2", img_name = "hires")
## [1] 19.08567
## attr(,"unit")
## [1] "um2/px"

Functions that convert areas are named similar to those that convert distances. If pixels are involved, the SPATA2 object must be specified as the scale factor is needed.

# numeric input is interpreted as pixel
as_millimeter2(input = c(200, 400, 4000, 50000), object = object_t313)
## Units: [mm^2]
## [1]  0.04241259  0.08482519  0.84825188 10.60314848
# if character, different units can be specified as input
as_centimeter2(input = c("4mm2", "400px"), object = object_t313)
## Units: [cm^2]
## [1] 0.0400000000 0.0008482519

5.2 Examples

Compute the area that is covered by the tissue sample.

getTissueArea(object_t313, unit = "mm2")
## 30.51777 [mm^2]

Compute the area that is covered by different image annotations and filter accordingly.

plotSpatialAnnotations(
  object = object_t313,
  square = TRUE
)
Fig.8 Image annotations whose area can be calculated.

Fig.8 Image annotations whose area can be calculated.

Extracting the values can be used for further subsetting.

ids <- getSpatAnnIds(object_t313)

spat_ann_areas <- 
  getSpatAnnArea(object = object_t313, ids = ids, unit = "mm2")

print(spat_ann_areas)
## Units: [mm^2]
##   necrotic_area necrotic_center   necrotic_edge  necrotic_edge2 
##       6.1856648       7.0992320       0.4775658       0.6342803
threshold <- units::set_units(x = 1, value = "mm2")

# keep only those with an area smaller than 1mm2
print(spat_ann_areas[spat_ann_areas < threshold])
## Units: [mm^2]
##  necrotic_edge necrotic_edge2 
##      0.4775658      0.6342803